/*********************************************************************
 * Copyright (C) 2002 Andrew Khan
 * <p>
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * <p>
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * <p>
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 ***************************************************************************/

package jxl.write.biff;

import jxl.CellType;
import jxl.SheetSettings;
import jxl.biff.*;
import jxl.common.Logger;
import jxl.write.Number;
import jxl.write.WritableCellFeatures;
import jxl.write.WritableSheet;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;

/**
 * Contains all the cells for a given row in a sheet
 */
class RowRecord extends WritableRecordData {
    /**
     * The logger
     */
    private static final Logger logger = Logger.getLogger(RowRecord.class);

    /**
     * The binary data
     */
    private byte[] data;
    /**
     * The cells which comprise this row
     */
    private CellValue[] cells;
    /**
     * The height of this row in 1/20ths of a point
     */
    private int rowHeight;
    /**
     * Flag to indicate whether this row is outline collapsed or not
     */
    private boolean collapsed;
    /**
     * The number of this row within the worksheet
     */
    private int rowNumber;
    /**
     * The number of columns in this row.  This is the largest column value + 1
     */
    private int numColumns;
    /**
     * The xfIndex for this row
     */
    private int xfIndex;
    /**
     * The style for this row
     */
    private XFRecord style;
    /**
     * Flag indicating that this row record has an default format
     */
    private boolean defaultFormat;
    /**
     * Flag indicating whether this row matches the default font height
     */
    private boolean matchesDefFontHeight;
    /**
     * The amount to grow the cells array by
     */
    private static final int growSize = 10;

    /**
     * The maximum integer value that can be squeezed into 30 bits
     */
    private static final int maxRKValue = 0x1fffffff;

    /**
     * The minimum integer value that can be squeezed into 30 bits
     */
    private static final int minRKValue = -0x20000000;

    /**
     * Indicates that the row is default height
     */
    private static int defaultHeightIndicator = 0xff;

    /**
     * The maximum number of columns
     */
    private static int maxColumns = 256;

    /**
     * The outline level of the row
     */
    private int outlineLevel;

    /**
     * Is this the icon indicator row of a group?
     */
    private boolean groupStart;

    /**
     * A handle back to the sheet
     */
    private WritableSheet sheet;

    /**
     * Constructs an empty row which has the specified row number
     *
     * @param rn the row number of this row
     */
    public RowRecord(int rn, WritableSheet ws) {
        super(Type.ROW);
        rowNumber = rn;
        cells = new CellValue[0];
        numColumns = 0;
        rowHeight = defaultHeightIndicator;
        collapsed = false;
        matchesDefFontHeight = true;
        sheet = ws;
    }

    /**
     * Sets the height of this row
     *
     * @param h the row height
     */
    public void setRowHeight(int h) {
        if (h == 0) {
            setCollapsed(true);
            matchesDefFontHeight = false;
        } else {
            rowHeight = h;
            matchesDefFontHeight = false;
        }
    }

    /**
     * Sets the row details based upon the readable row record passed in
     * Called when copying spreadsheets
     *
     * @param height the height of the row record in 1/20ths of a point
     * @param mdfh matches the default font height
     * @param col the collapsed status of the row
     * @param ol the outline level
     * @param gs the group start
     * @param xf the xfrecord for the row (NULL if no default is set)
     */
    void setRowDetails(int height,
                       boolean mdfh,
                       boolean col,
                       int ol,
                       boolean gs,
                       XFRecord xfr) {
        rowHeight = height;
        collapsed = col;
        matchesDefFontHeight = mdfh;
        outlineLevel = ol;
        groupStart = gs;

        if (xfr != null) {
            defaultFormat = true;
            style = xfr;
            xfIndex = style.getXFIndex();
        }
    }

    /**
     * Sets the collapsed status of this row
     *
     * @param c the collapsed flag
     */
    public void setCollapsed(boolean c) {
        collapsed = c;
    }

    /**
     * Gets the row number of this row
     *
     * @return the row number
     */
    public int getRowNumber() {
        return rowNumber;
    }

    /**
     * Adds a cell to this row, growing the array of cells as required
     *
     * @param cv the cell to add
     */
    public void addCell(CellValue cv) {
        int col = cv.getColumn();

        if (col >= maxColumns) {
            logger.warn("Could not add cell at " +
                    CellReferenceHelper.getCellReference(cv.getRow(),
                            cv.getColumn()) +
                    " because it exceeds the maximum column limit");
            return;
        }

        // Grow the array if needs be
        if (col >= cells.length) {
            CellValue[] oldCells = cells;
            cells = new CellValue[Math.max(oldCells.length + growSize, col + 1)];
            System.arraycopy(oldCells, 0, cells, 0, oldCells.length);
            oldCells = null;
        }

        // Remove any cell features from the cell being replaced
        if (cells[col] != null) {
            WritableCellFeatures wcf = cells[col].getWritableCellFeatures();
            if (wcf != null) {
                wcf.removeComment();

                // if the cell is part of a shared data validation,then don't remove
                // the validation
                if (wcf.getDVParser() != null &&
                        !wcf.getDVParser().extendedCellsValidation()) {
                    wcf.removeDataValidation();
                }
            }

        }

        cells[col] = cv;

        numColumns = Math.max(col + 1, numColumns);
    }

    /**
     * Removes a cell from this row
     *
     * @param col the column at which to remove the cell
     */
    public void removeCell(int col) {
        // Grow the array if needs be
        if (col >= numColumns) {
            return;
        }

        cells[col] = null;
    }

    /**
     * Writes out the row information data (but not the individual cells)
     *
     * @exception IOException
     * @param outputFile the output file
     */
    public void write(File outputFile) throws IOException {
        outputFile.write(this);
    }

    /**
     * Writes out all the cells in this row.  If more than three integer
     * values occur consecutively, then a MulRK record is used to group the
     * numbers
     *
     * @exception IOException
     * @param outputFile the output file
     */
    public void writeCells(File outputFile)
            throws IOException {
        // This is the list for integer values
        ArrayList integerValues = new ArrayList();
        boolean integerValue = false;

        // Write out all the records
        for (int i = 0; i < numColumns; i++) {
            integerValue = false;
            if (cells[i] != null) {
                // See if this cell is a 30-bit integer value (without additional
                // cell features)
                if (cells[i].getType() == CellType.NUMBER) {
                    Number nc = (Number) cells[i];
                    if (nc.getValue() == (int) nc.getValue() &&
                            nc.getValue() < maxRKValue &&
                            nc.getValue() > minRKValue &&
                            nc.getCellFeatures() == null) {
                        integerValue = true;
                    }
                }

                if (integerValue) {
                    // This cell is an integer, add it to the list
                    integerValues.add(cells[i]);
                } else {
                    // This cell is not an integer.  Write out whatever integers we
                    // have, and then write out this cell
                    writeIntegerValues(integerValues, outputFile);
                    outputFile.write(cells[i]);

                    // If the cell is a string formula, write out the string record
                    // immediately afterwards
                    if (cells[i].getType() == CellType.STRING_FORMULA) {
                        StringRecord sr = new StringRecord(cells[i].getContents());
                        outputFile.write(sr);
                    }
                }
            } else {
                // Cell does not exist.  Write out the list of integers that
                // we have
                writeIntegerValues(integerValues, outputFile);
            }
        }

        // All done.  Write out any remaining integer values
        writeIntegerValues(integerValues, outputFile);
    }

    /**
     * Writes out the list of integer values.  If there are more than three,
     * a MulRK record is used, otherwise a sequence of Numbers is used
     *
     * @exception IOException
     * @param outputFile the output file
     * @param integerValues the array of integer values
     */
    private void writeIntegerValues(ArrayList integerValues, File outputFile)
            throws IOException {
        if (integerValues.size() == 0) {
            return;
        }

        if (integerValues.size() >= 3) {
            // Write out as a MulRK record
            MulRKRecord mulrk = new MulRKRecord(integerValues);
            outputFile.write(mulrk);
        } else {
            // Write out as number records
            Iterator i = integerValues.iterator();
            while (i.hasNext()) {
                outputFile.write((CellValue) i.next());
            }
        }

        // Clear out the list of integerValues
        integerValues.clear();
    }

    /**
     * Gets the row data to output to file
     *
     * @return the binary data
     */
    public byte[] getData() {
        // Write out the row record
        byte[] data = new byte[16];

        // If the default row height has been changed in the sheet settings,
        // then we need to set the rowHeight on this row explicitly, as
        // specifying the "match default" flag doesn't work
        int rh = rowHeight;
        if (sheet.getSettings().getDefaultRowHeight() !=
                SheetSettings.DEFAULT_DEFAULT_ROW_HEIGHT) {
            // the default row height has been changed.  If this row does not
            // have a specific row height set on it, then set it to the default
            if (rh == defaultHeightIndicator) {
                rh = sheet.getSettings().getDefaultRowHeight();
            }
        }

        IntegerHelper.getTwoBytes(rowNumber, data, 0);
        IntegerHelper.getTwoBytes(numColumns, data, 4);
        IntegerHelper.getTwoBytes(rh, data, 6);

        int options = 0x100 + outlineLevel;

        if (groupStart) {
            options |= 0x10;
        }

        if (collapsed) {
            options |= 0x20;
        }

        if (!matchesDefFontHeight) {
            options |= 0x40;
        }

        if (defaultFormat) {
            options |= 0x80;
            options |= (xfIndex << 16);
        }

        IntegerHelper.getFourBytes(options, data, 12);

        return data;
    }

    /**
     * Gets the maximum column value which occurs in this row
     *
     * @return the maximum column value
     */
    public int getMaxColumn() {
        return numColumns;
    }

    /**
     * Gets the cell which occurs at the specified column value
     *
     * @param col the colun for which to return the cell
     * @return the cell value at the specified position, or null if the column
     *     is invalid
     */
    public CellValue getCell(int col) {
        return (col >= 0 && col < numColumns) ? cells[col] : null;
    }

    /**
     * Increments the row of this cell by one.  Invoked by the sheet when
     * inserting rows
     */
    void incrementRow() {
        rowNumber++;

        for (int i = 0; i < cells.length; i++) {
            if (cells[i] != null) {
                cells[i].incrementRow();
            }
        }
    }

    /**
     * Decrements the row of this cell by one.  Invoked by the sheet when
     * removing rows
     */
    void decrementRow() {
        rowNumber--;
        for (int i = 0; i < cells.length; i++) {
            if (cells[i] != null) {
                cells[i].decrementRow();
            }
        }
    }

    /**
     * Inserts a new column at the position specified.  If the max column length
     * is already reached, then the last column simply gets dropped
     *
     * @param col the column to insert
     */
    void insertColumn(int col) {
        // Don't bother doing anything unless there are cells after the
        // column to be inserted
        if (col >= numColumns) {
            return;
        }

        // Create a new array to hold the new column.  Grow it if need be
        CellValue[] oldCells = cells;

        if (numColumns >= cells.length - 1) {
            cells = new CellValue[oldCells.length + growSize];
        } else {
            cells = new CellValue[oldCells.length];
        }

        // Copy in everything up to the new column
        System.arraycopy(oldCells, 0, cells, 0, col);

        // Copy in the remaining cells
        System.arraycopy(oldCells, col, cells, col + 1, numColumns - col);

        // Increment all the internal column numbers by one
        for (int i = col + 1; i <= numColumns; i++) {
            if (cells[i] != null) {
                cells[i].incrementColumn();
            }
        }

        // Adjust the maximum column record
        numColumns = Math.min(numColumns + 1, maxColumns);
    }

    /**
     * Remove the new column at the position specified
     *
     * @param col the column to remove
     */
    void removeColumn(int col) {
        // Don't bother doing anything unless there are cells after the
        // column to be inserted
        if (col >= numColumns) {
            return;
        }

        // Create a new array to hold the new columns
        CellValue[] oldCells = cells;

        cells = new CellValue[oldCells.length];

        // Copy in everything up to the column
        System.arraycopy(oldCells, 0, cells, 0, col);

        // Copy in the remaining cells after the column
        System.arraycopy(oldCells, col + 1, cells, col, numColumns - (col + 1));

        // Decrement all the internal column numbers by one
        for (int i = col; i < numColumns; i++) {
            if (cells[i] != null) {
                cells[i].decrementColumn();
            }
        }

        // Adjust the maximum column record
        numColumns--;
    }

    /**
     * Interrogates whether this row is of default height
     *
     * @return TRUE if this is set to the default height, FALSE otherwise
     */
    public boolean isDefaultHeight() {
        return rowHeight == defaultHeightIndicator;
    }

    /**
     * Gets the height of the row
     *
     * @return the row height
     */
    public int getRowHeight() {
        return rowHeight;
    }

    /**
     * Queries whether the row is collapsed
     *
     * @return the collapsed indicator
     */
    public boolean isCollapsed() {
        return collapsed;
    }

    /**
     * Rationalizes the sheets xf index mapping
     * @param xfmapping the index mapping
     */
    void rationalize(IndexMapping xfmapping) {
        if (defaultFormat) {
            xfIndex = xfmapping.getNewIndex(xfIndex);
        }
    }

    /**
     * Accessor for the style.  The returned value is only non-null if the
     * default style is overridden
     *
     * @return the style
     */
    XFRecord getStyle() {
        return style;
    }

    /**
     * Accessor for the default format flag
     *
     * @return TRUE if this row has its own default format
     */
    boolean hasDefaultFormat() {
        return defaultFormat;
    }

    /**
     * Accessor for the matches default font height  flag
     *
     * @return TRUE if this row matches the default font height
     */
    boolean matchesDefaultFontHeight() {
        return matchesDefFontHeight;
    }

    /**
     * Accessor for the column's outline level
     *
     * @return the column's outline level
     */
    public int getOutlineLevel() {
        return outlineLevel;
    }

    /**
     * Accessor for row's groupStart state
     *
     * @return the row's groupStart state
     */
    public boolean getGroupStart() {
        return groupStart;
    }

    /**
     * Increments the row's outline level.  This is how groups are made as well
     */
    public void incrementOutlineLevel() {
        outlineLevel++;
    }

    /**
     * Decrements the row's outline level.  This removes it from a grouping
     * level.  If
     *  all outline levels are gone the uncollapse the row.
     */
    public void decrementOutlineLevel() {
        if (0 < outlineLevel) {
            outlineLevel--;
        }

        if (0 == outlineLevel) {
            collapsed = false;
        }
    }

    /**
     * Sets the row's outline level
     *
     * @param level the row's outline level
     */
    public void setOutlineLevel(int level) {
        outlineLevel = level;
    }

    /**
     *  Sets the row's group start state
     *
     * @param value the group start state
     */
    public void setGroupStart(boolean value) {
        groupStart = value;
    }
}










